package alogcore

import (
	"context"

	"github.com/opentracing/opentracing-go"
	"github.com/uber/jaeger-client-go"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"google.golang.org/grpc/metadata"
)

type logger struct {
	log *zap.Logger
}

type alogCtxMaker struct{}

var (
	log             ALogger
	alogCtxMakerKey = &alogCtxMaker{}
)

// It will initialize a default log
func init() {
	InitLogger("/tmp/galileo/unit_test/test.log",
		zap.DebugLevel,
		10,
		1,
		1,
		false,
		false,
		"GalileoDefaultLog")
}

// InitLogger use to init log
func InitLogger(filePath string, level zapcore.Level, maxSize int, maxBackup int, maxAge int, compress bool, stdout bool, serviceName string) {
	log = initLogger(filePath, level, maxSize, maxBackup, maxAge, compress, stdout, "json", Fields(NewStringField("serviceName", serviceName)))
}

// InitLoggerExt use to init log with extra options
func InitLoggerExt(filePath string, level string, maxSize int, maxBackup int, maxAge int, compress bool, stdout bool, encoder string, opt ...Option) {
	log = initLogger(filePath, GetLevel(level), maxSize, maxBackup, maxAge, compress, stdout, encoder, opt...)
}

// CreateCustomALogger Create the custom alogger, just use for write log to other log files, such as collection
// service output the collection log
func CreateCustomALogger(filePath string, level zapcore.Level, maxSize int, maxBackup int, maxAge int, compress bool, stdout bool, serviceName string) ALogger {
	return initLogger(filePath, level, maxSize, maxBackup, maxAge, compress, stdout, "json", Fields(NewStringField("serviceName", serviceName)))
}

func initLogger(filePath string, level zapcore.Level, maxSize int, maxBackup int, maxAge int, compress bool, stdout bool, encoder string, opt ...Option) *logger {
	log := &logger{}

	log.log = NewLogger(
		filePath,  /* file Path */
		level,     /* log level */
		maxSize,   /* max size*/
		maxBackup, /* max backup */
		maxAge,    /* max age */
		compress,  /* is compress? */
		stdout,
		encoder,
		opt..., /* service name */
	)
	return log
}

func (logger *logger) Info(ctx context.Context, msg string, fields ...Field) {
	zapFields := logger.processContext2(ctx, fields...)
	logger.log.Info(msg, zapFields...)
}

func (logger *logger) Debug(ctx context.Context, msg string, fields ...Field) {
	zapFields := logger.processContext2(ctx, fields...)
	logger.log.Debug(msg, zapFields...)
}

func (logger *logger) Warn(ctx context.Context, msg string, fields ...Field) {
	zapFields := logger.processContext2(ctx, fields...)
	logger.log.Warn(msg, zapFields...)
}

func (logger *logger) Error(ctx context.Context, msg string, fields ...Field) {
	zapFields := logger.processContext2(ctx, fields...)
	logger.log.Error(msg, zapFields...)
}

func (logger *logger) Panic(ctx context.Context, msg string, fields ...Field) {
	zapFields := logger.processContext2(ctx, fields...)
	logger.log.Panic(msg, zapFields...)
}

func (logger *logger) Fatal(ctx context.Context, msg string, fields ...Field) {
	zapFields := logger.processContext2(ctx, fields...)
	logger.log.Fatal(msg, zapFields...)
}

func (logger *logger) newContextForAlog(ctx context.Context) context.Context {
	ctxFields := &CtxField{}
	return context.WithValue(ctx, alogCtxMakerKey, ctxFields)
}

// Register the fields to alog
func (logger *logger) addFieldsToContext(ctx context.Context, fields ...Field) {
	ctxFields, ok := ctx.Value(alogCtxMakerKey).(*CtxField)
	if !ok {
		return
	}

	ctxFields.fields = append(ctxFields.fields, fields...)
}

// Info will write info type log
// Context will contain other information to log
func Info(ctx context.Context, msg string, fields ...Field) {
	log.Info(ctx, msg, fields...)
}

// Debug will write debug type log
// Context will contain other information to log
func Debug(ctx context.Context, msg string, fields ...Field) {
	log.Debug(ctx, msg, fields...)
}

// Warn will write warn type log
// Context will contain other information to log
func Warn(ctx context.Context, msg string, fields ...Field) {
	log.Warn(ctx, msg, fields...)
}

// Error will write error type log
// Context will contain other information to log
func Error(ctx context.Context, msg string, fields ...Field) {
	log.Error(ctx, msg, fields...)
}

// Panic will write panic type log
// Context will contain other information to log
func Panic(ctx context.Context, msg string, fields ...Field) {
	log.Panic(ctx, msg, fields...)
}

// Fatal will write fatal type log
// Context will contain other information to log
func Fatal(ctx context.Context, msg string, fields ...Field) {
	log.Fatal(ctx, msg, fields...)
}

// GetLogger Get alog logger
func GetLogger() ALogger {
	return log
}

// processContext2 will process the grpc metadata from context
func (logger *logger) processContext2(ctx context.Context, fields ...Field) []zap.Field {
	var (
		zapFields  []zap.Field
		skipFields = make(map[string]struct{})
	)

	ctxFields, ok := ctx.Value(alogCtxMakerKey).(*CtxField)

	if !ok || len(ctxFields.fields) <= 0 {
		return processContext(ctx, fields...)
	}

	// Remove duplication from fields
	fields = replaceRepeatFields(fields)

	for _, f := range fields {
		skipFields[f.zapField.Key] = struct{}{}
		zapFields = append(zapFields, f.zapField)
	}

	for _, f := range ctxFields.fields {
		if _, ok := skipFields[f.zapField.Key]; !ok {
			zapFields = append(zapFields, f.zapField)
			skipFields[f.zapField.Key] = struct{}{}
		}
	}

	return zapFields
}

// Add traceId to fields
func processContext(ctx context.Context, fields ...Field) []zap.Field {
	var zapField []zap.Field
	// Remove duplication from fields
	fields = replaceRepeatFields(fields)

	span := opentracing.SpanFromContext(ctx)
	if span != nil {
		if jspan, ok := span.Context().(jaeger.SpanContext); ok {
			zapField = append(zapField, zap.String("jaeger-trace-id", jspan.TraceID().String()))
		}
	}

	for _, key := range GetSupportTraceIDs() {
		if tmpField, ok := getTraceIDFromContext(ctx, key.GetValue()); ok {
			fields = append(fields, tmpField)
		}
	}

	for _, f := range fields {
		zapField = append(zapField, f.zapField)
	}

	return zapField
}

func getTraceIDFromContext(ctx context.Context, key string) (_ Field, ok bool) {
	m, ok := metadata.FromIncomingContext(ctx)
	if ok {
		if v, ok := m[key]; ok {
			if len(v) > 0 {
				eagleTraceField := NewStringField(key, v[0])
				return eagleTraceField, true
			}
		}
	}

	return Field{}, false
}

// replaceRepeatFields replace the repeat field, it will save the latest field and value
func replaceRepeatFields(fields []Field) []Field {
	fieldsCount := len(fields)
	result := make([]Field, 0, fieldsCount)
	tmpMap := make(map[string]struct{}, fieldsCount)
	if len(fields) <= 0 {
		return []Field{}
	}

	for i := fieldsCount - 1; i >= 0; i-- {
		tmpKey := fields[i].zapField.Key
		if v, ok := tmpMap[tmpKey]; !ok {
			result = append(result, fields[i])
			tmpMap[tmpKey] = v
		}
	}

	return result
}

// NewContextForAlog generate new context
func NewContextForAlog(ctx context.Context) context.Context {
	return log.newContextForAlog(ctx)
}

// AddFieldsToContext register field name to alog
func AddFieldsToContext(ctx context.Context, fields ...Field) {
	log.addFieldsToContext(ctx, fields...)
}

// GetLevel get convert level string to zapcore level
func GetLevel(logLevel string) zapcore.Level {
	switch logLevel {
	case "debug":
		return zapcore.DebugLevel
	case "info":
		return zapcore.InfoLevel
	case "warn":
		return zapcore.WarnLevel
	case "error":
		return zapcore.ErrorLevel
	case "panic":
		return zapcore.ErrorLevel
	case "fatal":
		return zapcore.FatalLevel
	default:
		return zapcore.ErrorLevel
	}
}
